package com.wyr.common.aspect;

import cn.hutool.core.util.IdUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.wyr.common.annotation.Log;
import com.wyr.common.enums.BusinessType;
import com.wyr.common.utils.IpUtil;
import com.wyr.common.vo.LogVo;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Date;
import java.util.Map;
import java.util.Optional;

/**
 * 操作日志记录处理
 *
 * @author wangyiren
 * @version 1.0
 * &#064;title  PrintMethodLog
 * &#064;description
 * &#064;date  2025/03/06 10:42
 */
@Slf4j
@Aspect
@Component
public class LogAspect {

    private static final String TRACE_ID = "traceId";
    private static final int MAX_LOG_LENGTH = 2000;

    ThreadLocal<Long> countTimeThreadLocal = new ThreadLocal<>();

    /**
     * 配置织入点
     */
    @Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping) || @annotation(org.springframework.web.bind.annotation.GetMapping)")
    public void logPointCut() {
    }

    /**
     * <p>
     * 在切点运行前执行该方法
     * </p>
     *
     * @param joinPoint
     * @return: void
     * @author: zsy
     * @time: 2021/2/4 17:53
     */
    @Before("logPointCut()")
    public void doBefore(JoinPoint joinPoint) {
        try {
            MDC.put(TRACE_ID, IdUtil.getSnowflakeNextIdStr());
            countTimeThreadLocal.set(System.currentTimeMillis());

            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            if (attributes == null) {
                return;
            }

            HttpServletRequest request = attributes.getRequest();
            Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
            Log logAnno = method.getAnnotation(Log.class);

            String operation = logAnno != null && StringUtils.isNotBlank(logAnno.title())
                    ? logAnno.title()
                    : method.getDeclaringClass().getSimpleName() + "." + method.getName();

            JSONObject paramsObj = extractParameters(joinPoint);

            log.info("请求开始：{} 请求者:{} url:{} 入参：{}", operation, IpUtil.getIp(), request.getRequestURI(), paramsObj);

        } catch (Exception e) {
            log.error("切面 doBefore 出现异常", e);
        }
    }

    /**
     * 处理完请求后执行
     *
     * @param joinPoint 切点
     */
    @AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
        handleLog(joinPoint, null, jsonResult);
    }

    /**
     * 拦截异常操作
     *
     * @param joinPoint 切点
     * @param e         异常
     */
    @AfterThrowing(value = "logPointCut()", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
        handleLog(joinPoint, e, null);
    }

    /**
     * 处理日志
     *
     * @param joinPoint
     * @param e
     * @param jsonResult
     * @return
     * &#064;description
     * @author wangyiren
     * &#064;date 10:32 下午 2021/8/19
     */
    protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult) {
        try {
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            if (attributes == null) {
                return;
            }

            HttpServletRequest request = attributes.getRequest();
            Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
            Log logAnno = getAnnotationLog(method);

            if (logAnno != null) {
                LogVo operateLog = buildLogVo(joinPoint, logAnno, request, jsonResult, e);
                // TODO: 调用日志服务持久化 operateLog，例如 logService.save(operateLog);
            }

            long startTime = Optional.ofNullable(countTimeThreadLocal.get()).orElse(System.currentTimeMillis());
            long cost = System.currentTimeMillis() - startTime;
            log.info("请求结束,耗时：{}ms,响应：{}", cost, truncateJson(jsonResult));

        } catch (Exception exp) {
            log.error("handleLog 异常: {}", exp.getMessage(), exp);
        } finally {
            countTimeThreadLocal.remove();
            MDC.remove(TRACE_ID);
        }
    }

    private JSONObject extractParameters(JoinPoint joinPoint) {
        JSONObject paramsObj = new JSONObject();
        Object[] args = joinPoint.getArgs();
        String[] paramNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();

        if (args != null && args.length == paramNames.length) {
            for (int i = 0; i < args.length; i++) {
                Object arg = args[i];
                if (isFilterObject(arg)) {
                    paramsObj.put(paramNames[i], arg.getClass().getSimpleName());
                } else {
                    paramsObj.put(paramNames[i], arg);
                }
            }
        }
        return paramsObj;
    }

    private LogVo buildLogVo(JoinPoint joinPoint, Log logAnno, HttpServletRequest request, Object jsonResult, Exception e) throws Exception {
        LogVo logVo = new LogVo();
        logVo.setCreateTime(new Date());
        logVo.setTitle(StringUtils.defaultIfBlank(logAnno.title(), joinPoint.getSignature().getName()));
        logVo.setIp(IpUtil.getIp(request));
        logVo.setUrl(request.getServletPath());
        logVo.setRequestMethod(request.getMethod());
        logVo.setMethod(joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()");
        logVo.setStatus(e == null ? "0" : "1");
        logVo.setErrorMsg(e != null ? StringUtils.substring(e.getMessage(), 0, MAX_LOG_LENGTH) : null);
        logVo.setJsonResult(truncateJson(jsonResult));

        if (logAnno.isSaveRequestData()) {
            String params = argsArrayToString(joinPoint.getArgs());
            if (logAnno.businessType() == BusinessType.LOGIN) {
                JSONObject json = JSONObject.parseObject(params);
                logVo.setUsername(json.getString("userName"));
            }
            logVo.setParam(StringUtils.substring(params, 0, MAX_LOG_LENGTH));
        }
        return logVo;
    }


    private String argsArrayToString(Object[] args) {
        StringBuilder sb = new StringBuilder();
        for (Object arg : args) {
            if (!isFilterObject(arg)) {
                try {
                    sb.append(JSON.toJSONString(arg)).append(" ");
                } catch (Exception e) {
                    log.warn("参数转换失败", e);
                }
            }
        }
        return sb.toString().trim();
    }


    private String truncateJson(Object jsonResult) {
        String json = JSON.toJSONString(jsonResult);
        return StringUtils.substring(json, 0, MAX_LOG_LENGTH);
    }

    private Log getAnnotationLog(Method method) {
        return method != null ? method.getAnnotation(Log.class) : null;
    }

    public boolean isFilterObject(final Object obj) {
        if (obj == null) {
            return true;
        }

        if (obj instanceof MultipartFile || obj instanceof HttpServletRequest || obj instanceof HttpServletResponse || obj instanceof BindingResult) {
            return true;
        }

        if (obj.getClass().isArray()) {
            return MultipartFile.class.isAssignableFrom(obj.getClass().getComponentType());
        }

        if (obj instanceof Collection) {
            return ((Collection<?>) obj).stream().anyMatch(o -> o instanceof MultipartFile);
        }

        if (obj instanceof Map) {
            return ((Map<?, ?>) obj).values().stream().anyMatch(v -> v instanceof MultipartFile);
        }

        return false;
    }


}
